package com.swak.threads;

import static com.codahale.metrics.MetricAttribute.COUNT;
import static com.codahale.metrics.MetricAttribute.M15_RATE;
import static com.codahale.metrics.MetricAttribute.M1_RATE;
import static com.codahale.metrics.MetricAttribute.M5_RATE;
import static com.codahale.metrics.MetricAttribute.MAX;
import static com.codahale.metrics.MetricAttribute.MEAN;
import static com.codahale.metrics.MetricAttribute.MEAN_RATE;
import static com.codahale.metrics.MetricAttribute.MIN;
import static com.codahale.metrics.MetricAttribute.P50;
import static com.codahale.metrics.MetricAttribute.P75;
import static com.codahale.metrics.MetricAttribute.P95;
import static com.codahale.metrics.MetricAttribute.P98;
import static com.codahale.metrics.MetricAttribute.P99;
import static com.codahale.metrics.MetricAttribute.P999;
import static com.codahale.metrics.MetricAttribute.STDDEV;

import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Counting;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.MetricAttribute;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.swak.metrics.impl.CodahaleMetricsFactory;
import com.swak.metrics.metas.MapMeta;
import com.swak.utils.Lists;
import com.swak.utils.StringUtils;

/**
 * 指标统计
 * 
 * @author DELL
 */
@Service
public class MetricsDumpService implements InitializingBean {

	private final long durationFactor;
	private final String durationUnit;
	private final long rateFactor;
	private final String rateUnit;
	private MetricRegistry registry;

	@Autowired(required = false)
	private CodahaleMetricsFactory metricsFactory;

	public MetricsDumpService() {
		this.rateFactor = TimeUnit.SECONDS.toSeconds(1);
		this.rateUnit = calculateRateUnit(TimeUnit.SECONDS);
		this.durationFactor = TimeUnit.MILLISECONDS.toNanos(1);
		this.durationUnit = TimeUnit.MILLISECONDS.toString().toLowerCase(Locale.US);
	}

	public MetricsDumpService(MetricRegistry registry) {
		this();
		this.registry = registry;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (metricsFactory != null) {
			this.registry = metricsFactory.metricRegistry();
		}
	}

	protected String getRateUnit() {
		return rateUnit;
	}

	protected String getDurationUnit() {
		return durationUnit;
	}

	protected double convertDuration(double duration) {
		return duration / durationFactor;
	}

	protected double convertRate(double rate) {
		return rate * rateFactor;
	}

	private String calculateRateUnit(TimeUnit unit) {
		final String s = unit.toString().toLowerCase(Locale.US);
		return s.substring(0, s.length() - 1);
	}

	public List<MapMeta> metricsDump(String name) {
		if (registry == null) {
			return Lists.newArrayList();
		}
		MetricFilter filter = StringUtils.isNotBlank(name) ? MetricFilter.startsWith(name) : MetricFilter.ALL;
		return report(registry.getGauges(filter), registry.getCounters(filter), registry.getHistograms(filter),
				registry.getMeters(filter), registry.getTimers(filter));
	}

	private List<MapMeta> report(@SuppressWarnings("rawtypes") SortedMap<String, Gauge> gauges,
			SortedMap<String, Counter> counters, SortedMap<String, Histogram> histograms,
			SortedMap<String, Meter> meters, SortedMap<String, Timer> timers) {

		List<MapMeta> metas = Lists.newArrayList();

		for (@SuppressWarnings("rawtypes")
		Entry<String, Gauge> entry : gauges.entrySet()) {
			metas.add(logGauge(entry.getKey(), entry.getValue()));
		}

		for (Entry<String, Counter> entry : counters.entrySet()) {
			metas.add(logCounter(entry.getKey(), entry.getValue()));
		}

		for (Entry<String, Histogram> entry : histograms.entrySet()) {
			metas.add(logHistogram(entry.getKey(), entry.getValue()));
		}

		for (Entry<String, Meter> entry : meters.entrySet()) {
			metas.add(logMeter(entry.getKey(), entry.getValue()));
		}

		for (Entry<String, Timer> entry : timers.entrySet()) {
			metas.add(logTimer(entry.getKey(), entry.getValue()));
		}

		return metas;
	}

	private MapMeta logTimer(String name, Timer timer) {
		final Snapshot snapshot = timer.getSnapshot();
		MapMeta meta = MapMeta.of();
		append(meta, "type", "TIMER");
		append(meta, "name", prefix(name));
		appendCountIfEnabled(meta, timer);
		appendLongDurationIfEnabled(meta, MIN, snapshot::getMin);
		appendLongDurationIfEnabled(meta, MAX, snapshot::getMax);
		appendDoubleDurationIfEnabled(meta, MEAN, snapshot::getMean);
		appendDoubleDurationIfEnabled(meta, STDDEV, snapshot::getStdDev);
		appendDoubleDurationIfEnabled(meta, P50, snapshot::getMedian);
		appendDoubleDurationIfEnabled(meta, P75, snapshot::get75thPercentile);
		appendDoubleDurationIfEnabled(meta, P95, snapshot::get95thPercentile);
		appendDoubleDurationIfEnabled(meta, P98, snapshot::get98thPercentile);
		appendDoubleDurationIfEnabled(meta, P99, snapshot::get99thPercentile);
		appendDoubleDurationIfEnabled(meta, P999, snapshot::get999thPercentile);
		appendMetered(meta, timer);
		append(meta, "rate_unit", getRateUnit());
		append(meta, "duration_unit", getDurationUnit());
		return meta;
	}

	private MapMeta logMeter(String name, Meter meter) {
		MapMeta meta = MapMeta.of();
		append(meta, "type", "METER");
		append(meta, "name", prefix(name));
		appendCountIfEnabled(meta, meter);
		appendMetered(meta, meter);
		append(meta, "rate_unit", getRateUnit());
		return meta;
	}

	private MapMeta logHistogram(String name, Histogram histogram) {
		final Snapshot snapshot = histogram.getSnapshot();
		MapMeta meta = MapMeta.of();
		append(meta, "type", "HISTOGRAM");
		append(meta, "name", prefix(name));
		appendCountIfEnabled(meta, histogram);
		appendLongIfEnabled(meta, MIN, snapshot::getMin);
		appendLongIfEnabled(meta, MAX, snapshot::getMax);
		appendDoubleIfEnabled(meta, MEAN, snapshot::getMean);
		appendDoubleIfEnabled(meta, STDDEV, snapshot::getStdDev);
		appendDoubleIfEnabled(meta, P50, snapshot::getMedian);
		appendDoubleIfEnabled(meta, P75, snapshot::get75thPercentile);
		appendDoubleIfEnabled(meta, P95, snapshot::get95thPercentile);
		appendDoubleIfEnabled(meta, P98, snapshot::get98thPercentile);
		appendDoubleIfEnabled(meta, P99, snapshot::get99thPercentile);
		appendDoubleIfEnabled(meta, P999, snapshot::get999thPercentile);
		return meta;
	}

	private MapMeta logCounter(String name, Counter counter) {
		MapMeta meta = MapMeta.of();
		append(meta, "type", "COUNTER");
		append(meta, "name", prefix(name));
		append(meta, COUNT.getCode(), counter.getCount());
		return meta;
	}

	private MapMeta logGauge(String name, Gauge<?> gauge) {
		MapMeta meta = MapMeta.of();
		append(meta, "type", "GAUGE");
		append(meta, "name", prefix(name));
		append(meta, "value", gauge.getValue());
		return meta;
	}

	private void appendLongDurationIfEnabled(Map<String, Object> meta, MetricAttribute metricAttribute,
			Supplier<Long> durationSupplier) {
		append(meta, metricAttribute.getCode(), convertDuration(durationSupplier.get()));
	}

	private void appendDoubleDurationIfEnabled(Map<String, Object> meta, MetricAttribute metricAttribute,
			Supplier<Double> durationSupplier) {
		append(meta, metricAttribute.getCode(), convertDuration(durationSupplier.get()));
	}

	private void appendLongIfEnabled(Map<String, Object> meta, MetricAttribute metricAttribute,
			Supplier<Long> valueSupplier) {
		append(meta, metricAttribute.getCode(), valueSupplier.get());
	}

	private void appendDoubleIfEnabled(Map<String, Object> meta, MetricAttribute metricAttribute,
			Supplier<Double> valueSupplier) {
		append(meta, metricAttribute.getCode(), valueSupplier.get());
	}

	private void appendCountIfEnabled(Map<String, Object> meta, Counting counting) {
		meta.put(COUNT.getCode(), counting.getCount());
	}

	private void appendMetered(Map<String, Object> meta, Metered meter) {
		appendRateIfEnabled(meta, M1_RATE, meter::getOneMinuteRate);
		appendRateIfEnabled(meta, M5_RATE, meter::getFiveMinuteRate);
		appendRateIfEnabled(meta, M15_RATE, meter::getFifteenMinuteRate);
		appendRateIfEnabled(meta, MEAN_RATE, meter::getMeanRate);
	}

	private void appendRateIfEnabled(Map<String, Object> meta, MetricAttribute metricAttribute,
			Supplier<Double> rateSupplier) {
		append(meta, metricAttribute.getCode(), convertRate(rateSupplier.get()));
	}

	private void append(Map<String, Object> meta, String key, long value) {
		meta.put(key, value);
	}

	private void append(Map<String, Object> meta, String key, double value) {
		meta.put(key, value);
	}

	private void append(Map<String, Object> meta, String key, String value) {
		meta.put(key, value);
	}

	private void append(Map<String, Object> meta, String key, Object value) {
		meta.put(key, value);
	}

	private String prefix(String... components) {
		return MetricRegistry.name(StringUtils.EMPTY, components);
	}
}